﻿using System;
using System.Collections.Specialized;
using System.Linq;
using System.Web.Configuration;
using System.Web.Security;
using AspAuthentication.Core;
using AspAuthentication.Interfaces;
using AspAuthentication.Persistence;
using System.ComponentModel.DataAnnotations;

namespace AspAuthentication.Providers
{
    /// <summary>
    /// Defines the class PortalMembershipProvider
    /// </summary>
    public class PortalMembershipProvider : MembershipProvider, IPortalMembershipProvider
    {
        bool _requiresQuestionAndAnswer;
        private int _minRequiredPasswordLength;
        private bool _enablePasswordRetrieval;
        private bool _enablePasswordReset;
        private int _maxInvalidPasswordAttempts;
        private int _passwordAttemptWindow;
        private bool _requiresUniqueEmail;
        private MembershipPasswordFormat _passwordFormat;
        private int _minRequiredNonAlphanumericCharacters;
        private string _passwordStrengthRegularExpression;
        private string _description;
        private string _name;
        private IUnitOfWork _unitOfWork;
        /// <summary>
        /// Gets the unit of work.
        /// </summary>
        /// <value>The unit of work.</value>
        public IUnitOfWork UnitOfWork
        {
            get { return _unitOfWork ?? (_unitOfWork = new UnitOfWork(ConnectionStringName, CreateDatabaseObjects)); }
        }
        /// <summary>
        /// Gets or sets a value indicating whether [create database objects].
        /// </summary>
        /// <value>
        /// 	<c>true</c> if [create database objects]; otherwise, <c>false</c>.
        /// </value>
        public bool CreateDatabaseObjects { get; set; }

        /// <summary>
        /// Creates the user.
        /// </summary>
        /// <param name="user">The user.</param>
        /// <param name="status">The status.</param>
        /// <returns></returns>
        public MembershipUser CreateUser(DomainUser user, out MembershipCreateStatus status)
        {
            var currentDate = DateTime.Now;
            var tempPassword = user.Password;
            user.CreationDate = currentDate;
            try
            {
                PerformDomainValidation(user);
                user.Password = HandlePasswordHash(user.Password);
                user = UnitOfWork.Create(user);
                status = MembershipCreateStatus.Success;
            }
            catch (Exception exception)
            {
                status = HandleUserCreationExceptions(exception);
            }
            user.Password = tempPassword;
            return user;
        }

        /// <summary>
        /// Creates the user.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <param name="email">The email.</param>
        /// <param name="status">The status.</param>
        /// <returns></returns>
        public MembershipUser CreateUser(string username, string password, string email, out MembershipCreateStatus status)
        {
            return CreateUser(username, password, email, "", "", true, Name, out status);
        }

        /// <summary>
        /// Handles the password hash.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        public string HandlePasswordHash(string password)
        {
            var hashedPassword = password;

           switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Hashed:
                    hashedPassword = FormsAuthentication.HashPasswordForStoringInConfigFile(password, FormsAuthPasswordFormat.SHA1.ToString());
                    break;
                //case MembershipPasswordFormat.Encrypted:
                //    hashedPassword = ;
                //    break;
            }
            
            return hashedPassword;
        }

        /// <summary>
        /// Adds a new membership user to the data source.
        /// </summary>
        /// <param name="username">The user name for the new user.</param>
        /// <param name="password">The password for the new user.</param>
        /// <param name="email">The e-mail address for the new user.</param>
        /// <param name="passwordQuestion">The password question for the new user.</param>
        /// <param name="passwordAnswer">The password answer for the new user</param>
        /// <param name="isApproved">Whether or not the new user is approved to be validated.</param>
        /// <param name="providerUserKey">The unique identifier from the membership data source for the user.</param>
        /// <param name="status">A <see cref="T:System.Web.Security.MembershipCreateStatus"/> enumeration value indicating whether the user was created successfully.</param>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUser"/> object populated with the information for the newly created user.
        /// </returns>
        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            var currentDate = DateTime.Now;
            var user = new DomainUser(Name, username, providerUserKey, email, passwordQuestion, "", isApproved, false,currentDate, currentDate, currentDate, currentDate, currentDate, password,passwordAnswer);

            return CreateUser(user, out status);
        }

        /// <summary>
        /// Processes a request to update the password question and answer for a membership user.
        /// </summary>
        /// <param name="username">The user to change the password question and answer for.</param>
        /// <param name="password">The password for the specified user.</param>
        /// <param name="newPasswordQuestion">The new password question for the specified user.</param>
        /// <param name="newPasswordAnswer">The new password answer for the specified user.</param>
        /// <returns>
        /// true if the password question and answer are updated successfully; otherwise, false.
        /// </returns>
        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            var processedPassword = HandlePasswordHash(password);

            var domainUser = UnitOfWork.RetrieveDomainObject<DomainUser>(u => u.UserName == username && u.Password == processedPassword);
            domainUser.PasswordQuestion = newPasswordQuestion;
            domainUser.PasswordAnswer = newPasswordAnswer;
            UnitOfWork.Update(domainUser);
            return true;
        }

        /// <summary>
        /// Gets the password for the specified user name from the data source.
        /// </summary>
        /// <param name="username">The user to retrieve the password for.</param>
        /// <param name="answer">The password answer for the user.</param>
        /// <returns>
        /// The password for the specified user name.
        /// </returns>
        public override string GetPassword(string username, string answer)
        {
            return UnitOfWork.RetrieveDomainObject<DomainUser>(u => u.UserName == username && u.PasswordAnswer == answer).Password;
        }

        /// <summary>
        /// Processes a request to update the password for a membership user.
        /// </summary>
        /// <param name="username">The user to update the password for.</param>
        /// <param name="oldPassword">The current password for the specified user.</param>
        /// <param name="newPassword">The new password for the specified user.</param>
        /// <returns>
        /// true if the password was updated successfully; otherwise, false.
        /// </returns>
        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            var processedPassword = HandlePasswordHash(oldPassword);
            var domainUser = UnitOfWork.RetrieveDomainObject<DomainUser>(u => u.UserName == username && u.Password == processedPassword);
            domainUser.Password = HandlePasswordHash(newPassword);
            UnitOfWork.Update(domainUser);
            return true;
        }

        /// <summary>
        /// Resets a user's password to a new, automatically generated password.
        /// </summary>
        /// <param name="username">The user to reset the password for.</param>
        /// <param name="answer">The password answer for the specified user.</param>
        /// <returns>The new password for the specified user.</returns>
        public override string ResetPassword(string username, string answer)
        {
            var domainUser = UnitOfWork.RetrieveDomainObject<DomainUser>(u => u.UserName == username && u.PasswordAnswer == answer);
            //TODO:developed an algorithm to randomly generate this in DomainUser object
            var password = HandlePasswordHash("Password123456");
            domainUser.Password = password;
            UnitOfWork.Update(domainUser);
            return password;
        }

        /// <summary>
        /// Updates information about a user in the data source.
        /// </summary>
        /// <param name="user">A <see cref="T:System.Web.Security.MembershipUser"/> object that represents the user to update and the updated information for the user.</param>
        public override void UpdateUser(MembershipUser user)
        {
            UnitOfWork.Update((DomainUser)user);
        }

        /// <summary>
        /// Verifies that the specified user name and password exist in the data source.
        /// </summary>
        /// <param name="username">The name of the user to validate.</param>
        /// <param name="password">The password for the specified user.</param>
        /// <returns>
        /// true if the specified username and password are valid; otherwise, false.
        /// </returns>
        public override bool ValidateUser(string username, string password)
        {
            var processedPassword = HandlePasswordHash(password);
            var userList = UnitOfWork.Search<DomainUser>(u => u.UserName == username && u.Password == processedPassword);
            return userList.Count > 0;
        }

        /// <summary>
        /// Logins the user.
        /// </summary>
        /// <param name="username">The username.</param>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        public bool LoginUser(string username, string password)
        {
            bool succeeded = false;
            var processedPassword = HandlePasswordHash(password);
            var userList = UnitOfWork.Search<DomainUser>(u => u.UserName == username && u.Password == processedPassword);
            if (userList.Count > 0)
            {
                var user = userList.First();
                user.IsOnline = true;
                user.LastLoginDate = DateTime.Now;
                user.LastActivityDate = DateTime.Now;
                UnitOfWork.Update(user);
                succeeded = true;
            }

            return succeeded;
        }

        /// <summary>
        /// Logouts the user.
        /// </summary>
        public void LogoutUser()
        {
            var user = Membership.GetUser() as DomainUser;
            if (user == null) return;
            user.IsOnline = false;
            UnitOfWork.Update(user);
        }

        /// <summary>
        /// Clears a lock so that the membership user can be validated.
        /// </summary>
        /// <param name="userName">The membership user whose lock status you want to clear.</param>
        /// <returns>
        /// true if the membership user was successfully unlocked; otherwise, false.
        /// </returns>
        public override bool UnlockUser(string userName)
        {
            var user = UnitOfWork.RetrieveDomainObject<DomainUser>(u => u.UserName == userName);
            user.IsLockedOut = false;
            UnitOfWork.Update(user);
            return true;
        }

        /// <summary>
        /// Gets user information from the data source based on the unique identifier for the membership user. Provides an option to update the last-activity date/time stamp for the user.
        /// </summary>
        /// <param name="providerUserKey">The unique identifier for the membership user to get information for.</param>
        /// <param name="userIsOnline">true to update the last-activity date/time stamp for the user; false to return user information without updating the last-activity date/time stamp for the user.</param>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUser"/> object populated with the specified user's information from the data source.
        /// </returns>
        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            return GetUser(providerUserKey.ToString(), userIsOnline);
        }

        /// <summary>
        /// Gets information from the data source for a user. Provides an option to update the last-activity date/time stamp for the user.
        /// </summary>
        /// <param name="username">The name of the user to get information for.</param>
        /// <param name="userIsOnline">true to update the last-activity date/time stamp for the user; false to return user information without updating the last-activity date/time stamp for the user.</param>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUser"/> object populated with the specified user's information from the data source.
        /// </returns>
        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            return UnitOfWork.RetrieveDomainObject<DomainUser>(u => u.UserName == username && u.IsOnline == userIsOnline);
        }

        /// <summary>
        /// Gets the user name associated with the specified e-mail address.
        /// </summary>
        /// <param name="email">The e-mail address to search for.</param>
        /// <returns>
        /// The user name associated with the specified e-mail address. If no match is found, return null.
        /// </returns>
        public override string GetUserNameByEmail(string email)
        {
            return UnitOfWork.RetrieveDomainObject<DomainUser>(u => u.Email == email).UserName;
        }

        /// <summary>
        /// Removes a user from the membership data source.
        /// </summary>
        /// <param name="username">The name of the user to delete.</param>
        /// <param name="deleteAllRelatedData">true to delete data related to the user from the database; false to leave data related to the user in the database.</param>
        /// <returns>
        /// true if the user was successfully deleted; otherwise, false.
        /// </returns>
        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            UnitOfWork.DeleteDomainUser(username);
            return true;
        }

        /// <summary>
        /// Gets a collection of all the users in the data source in pages of data.
        /// </summary>
        /// <param name="pageIndex">The index of the page of results to return. <paramref name="pageIndex"/> is zero-based.</param>
        /// <param name="pageSize">The size of the page of results to return.</param>
        /// <param name="totalRecords">The total number of matched users.</param>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUserCollection"/> collection that contains a page of <paramref name="pageSize"/><see cref="T:System.Web.Security.MembershipUser"/> objects beginning at the page specified by <paramref name="pageIndex"/>.
        /// </returns>
        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            var list = UnitOfWork.Search<DomainUser>();
            var collection = new MembershipUserCollection();
            foreach (var item in list)
            {
                collection.Add(item.ToMembershipUser);
            }
            totalRecords = collection.Count;
            return collection;
        }

        /// <summary>
        /// Gets the number of users currently accessing the application.
        /// </summary>
        /// <returns>
        /// The number of users currently accessing the application.
        /// </returns>
        public override int GetNumberOfUsersOnline()
        {
            return UnitOfWork.Search<DomainUser>(u => u.IsOnline).Count;
        }

        /// <summary>
        /// Gets a collection of membership users where the user name contains the specified user name to match.
        /// </summary>
        /// <param name="usernameToMatch">The user name to search for.</param>
        /// <param name="pageIndex">The index of the page of results to return. <paramref name="pageIndex"/> is zero-based.</param>
        /// <param name="pageSize">The size of the page of results to return.</param>
        /// <param name="totalRecords">The total number of matched users.</param>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUserCollection"/> collection that contains a page of <paramref name="pageSize"/><see cref="T:System.Web.Security.MembershipUser"/> objects beginning at the page specified by <paramref name="pageIndex"/>.
        /// </returns>
        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            var list = UnitOfWork.Search<DomainUser>(u => u.UserName.Contains(usernameToMatch));
            var collection = new MembershipUserCollection();
            foreach (var item in list)
            {
                collection.Add(item.ToMembershipUser);
            }
            totalRecords = collection.Count;
            return collection;
        }

        /// <summary>
        /// Gets a collection of membership users where the e-mail address contains the specified e-mail address to match.
        /// </summary>
        /// <param name="emailToMatch">The e-mail address to search for.</param>
        /// <param name="pageIndex">The index of the page of results to return. <paramref name="pageIndex"/> is zero-based.</param>
        /// <param name="pageSize">The size of the page of results to return.</param>
        /// <param name="totalRecords">The total number of matched users.</param>
        /// <returns>
        /// A <see cref="T:System.Web.Security.MembershipUserCollection"/> collection that contains a page of <paramref name="pageSize"/><see cref="T:System.Web.Security.MembershipUser"/> objects beginning at the page specified by <paramref name="pageIndex"/>.
        /// </returns>
        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            var list = UnitOfWork.Search<DomainUser>(u => u.Email.Contains(emailToMatch));
            var collection = new MembershipUserCollection();
            foreach (var item in list)
            {
                collection.Add(item.ToMembershipUser);
            }
            totalRecords = collection.Count;
            return collection;
        }

        /// <summary>
        /// Initializes the provider.
        /// </summary>
        /// <param name="name">The friendly name of the provider.</param>
        /// <param name="config">A collection of the name/value pairs representing the provider-specific attributes specified in the configuration for this provider.</param>
        /// <exception cref="T:System.ArgumentNullException">The name of the provider is null.</exception>
        /// <exception cref="T:System.ArgumentException">The name of the provider has a length of zero.</exception>
        /// <exception cref="T:System.InvalidOperationException">An attempt is made to call <see cref="M:System.Configuration.Provider.ProviderBase.Initialize(System.String,System.Collections.Specialized.NameValueCollection)"/> on a provider after the provider has already been initialized.</exception>
        public override void Initialize(string name, NameValueCollection config)
        {
            base.Initialize(name, config);
            _requiresQuestionAndAnswer  = Convert.ToBoolean(ConfigHelper.SafeConfigRead("requiresQuestionAndAnswer", config));
            _minRequiredPasswordLength = ConfigHelper.SafeConfigIntRead("minRequiredPasswordLength", config,6);
            _enablePasswordRetrieval    = Convert.ToBoolean(ConfigHelper.SafeConfigRead("enablePasswordRetrieval", config));
            _description                = ConfigHelper.SafeConfigRead("description", config);
            _enablePasswordReset        = Convert.ToBoolean(ConfigHelper.SafeConfigRead("enablePasswordReset", config));
            _maxInvalidPasswordAttempts = ConfigHelper.SafeConfigIntRead("maxInvalidPasswordAttempts", config,5);
            _passwordAttemptWindow = ConfigHelper.SafeConfigIntRead("passwordAttemptWindow", config,0);
            _requiresUniqueEmail        = Convert.ToBoolean(ConfigHelper.SafeConfigRead("requiresUniqueEmail", config));

            var passwordFormat = ConfigHelper.SafeConfigRead("passwordFormat", config, "Clear");
            if (!Enum.TryParse(passwordFormat, true, out _passwordFormat))
            {
                _passwordFormat = MembershipPasswordFormat.Clear;
            }

            _minRequiredNonAlphanumericCharacters = ConfigHelper.SafeConfigIntRead("minRequiredNonAlphanumericCharacters", config,0);
            _passwordStrengthRegularExpression = ConfigHelper.SafeConfigRead("passwordStrengthRegularExpression", config);
            ApplicationName = ConfigHelper.SafeConfigRead("applicationName", config);
            _name = name;
            ConnectionStringName = ConfigHelper.SafeConfigRead("connectionStringName", config);
            CreateDatabaseObjects = ConfigHelper.SafeConfigBooleanRead("createDatabaseObjects", config,false);
        }

        
        #region Properties
        /// <summary>
        /// Indicates whether the membership provider is configured to allow users to retrieve their passwords.
        /// </summary>
        /// <value></value>
        /// <returns>true if the membership provider is configured to support password retrieval; otherwise, false. The default is false.</returns>
        public override bool EnablePasswordRetrieval
        {
            get { return _enablePasswordRetrieval; }
        }

        /// <summary>
        /// Indicates whether the membership provider is configured to allow users to reset their passwords.
        /// </summary>
        /// <value></value>
        /// <returns>true if the membership provider supports password reset; otherwise, false. The default is true.</returns>
        public override bool EnablePasswordReset
        {
            get { return _enablePasswordReset; }
        }

        /// <summary>
        /// Gets a value indicating whether the membership provider is configured to require the user to answer a password question for password reset and retrieval.
        /// </summary>
        /// <value></value>
        /// <returns>true if a password answer is required for password reset and retrieval; otherwise, false. The default is true.</returns>
        public override bool RequiresQuestionAndAnswer
        {
            get { return _requiresQuestionAndAnswer; }
        }

        /// <summary>
        /// The name of the application using the custom membership provider.
        /// </summary>
        /// <value></value>
        /// <returns>The name of the application using the custom membership provider.</returns>
        public override string ApplicationName { get; set; }

        /// <summary>
        /// Gets the number of invalid password or password-answer attempts allowed before the membership user is locked out.
        /// </summary>
        /// <value></value>
        /// <returns>The number of invalid password or password-answer attempts allowed before the membership user is locked out.</returns>
        public override int MaxInvalidPasswordAttempts
        {
            get { return _maxInvalidPasswordAttempts; }
        }

        /// <summary>
        /// Gets the number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out.
        /// </summary>
        /// <value></value>
        /// <returns>The number of minutes in which a maximum number of invalid password or password-answer attempts are allowed before the membership user is locked out.</returns>
        public override int PasswordAttemptWindow
        {
            get { return _passwordAttemptWindow; }
        }

        /// <summary>
        /// Gets a value indicating whether the membership provider is configured to require a unique e-mail address for each user name.
        /// </summary>
        /// <value></value>
        /// <returns>true if the membership provider requires a unique e-mail address; otherwise, false. The default is true.</returns>
        public override bool RequiresUniqueEmail
        {
            get { return _requiresUniqueEmail; }
        }

        /// <summary>
        /// Gets a value indicating the format for storing passwords in the membership data store.
        /// </summary>
        /// <value></value>
        /// <returns>One of the <see cref="T:System.Web.Security.MembershipPasswordFormat"/> values indicating the format for storing passwords in the data store.</returns>
        public override MembershipPasswordFormat PasswordFormat
        {
            get { return _passwordFormat; }
        }

        /// <summary>
        /// Gets the minimum length required for a password.
        /// </summary>
        /// <value></value>
        /// <returns>The minimum length required for a password. </returns>
        public override int MinRequiredPasswordLength
        {
            get { return _minRequiredPasswordLength; }
        }

        /// <summary>
        /// Gets the minimum number of special characters that must be present in a valid password.
        /// </summary>
        /// <value></value>
        /// <returns>The minimum number of special characters that must be present in a valid password.</returns>
        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return _minRequiredNonAlphanumericCharacters; }
        }

        /// <summary>
        /// Gets the regular expression used to evaluate a password.
        /// </summary>
        /// <value></value>
        /// <returns>A regular expression used to evaluate a password.</returns>
        public override string PasswordStrengthRegularExpression
        {
            get { return _passwordStrengthRegularExpression; }
        }
        /// <summary>
        /// Gets a brief, friendly description suitable for display in administrative tools or other user interfaces (UIs).
        /// </summary>
        /// <value></value>
        /// <returns>A brief, friendly description suitable for display in administrative tools or other UIs.</returns>
        public override string Description
        {
            get { return _description; }
        }
        /// <summary>
        /// Gets the friendly name used to refer to the provider during configuration.
        /// </summary>
        /// <value></value>
        /// <returns>The friendly name used to refer to the provider during configuration.</returns>
        public override string Name
        {
            get { return _name; }
        }
        /// <summary>
        /// Gets or sets the name of the connection string.
        /// </summary>
        /// <value>The name of the connection string.</value>
        public string ConnectionStringName { get; set; }
        #endregion

        private static MembershipCreateStatus HandleUserCreationExceptions(Exception exception)
        {
            var status = MembershipCreateStatus.ProviderError;
           
            var validationException = exception as ValidationException;

            if (validationException != null)
            {
                if (validationException.Message.Contains("The Password field is required."))
                {
                    status = MembershipCreateStatus.InvalidPassword;
                }
                else if (validationException.Message.Contains("The Email field is required."))
                {
                    status = MembershipCreateStatus.InvalidEmail;
                }
            }
            const string violationOfPrimaryKeyConstraint = "Violation of PRIMARY KEY constraint";
            if (
                (exception.InnerException != null &&
                    exception.InnerException.Message.Contains(violationOfPrimaryKeyConstraint))
               ||
                (exception.InnerException!=null && exception.InnerException.InnerException!=null &&
                exception.InnerException.InnerException.Message.Contains(violationOfPrimaryKeyConstraint)
                )
                
                )
            {
                status = MembershipCreateStatus.DuplicateUserName;
            }

            return status;
        }

        private static void PerformDomainValidation(DomainUser membershipUser)
        {
            var validationContext = new ValidationContext(membershipUser, null, null);
            Validator.ValidateObject(membershipUser, validationContext, true);
        }

    }

}
